
#include "proto_file.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <limits.h>
#include <stddef.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fstream>
#include <string>

namespace common
{

using std::istreambuf_iterator;
using std::string;
using std::vector;

bool SetProtoToASCIIFile(const google::protobuf::Message &message,
                         int file_descriptor)
{
    using google::protobuf::TextFormat;
    using google::protobuf::io::FileOutputStream;
    using google::protobuf::io::ZeroCopyOutputStream;
    if (file_descriptor < 0)
    {
        std::cerr << "Invalid file descriptor.";
        return false;
    }
    ZeroCopyOutputStream *output = new FileOutputStream(file_descriptor);
    bool success = TextFormat::Print(message, output);
    delete output;
    close(file_descriptor);
    return success;
}

bool SetProtoToASCIIFile(const google::protobuf::Message &message,
                         const std::string &file_name)
{
    int fd = open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
    if (fd < 0)
    {
        std::cerr << "Unable to open file " << file_name << " to write.";
        return false;
    }
    return SetProtoToASCIIFile(message, fd);
}

bool GetProtoFromASCIIFile(const std::string &file_name,
                           google::protobuf::Message *message)
{
    using google::protobuf::TextFormat;
    using google::protobuf::io::FileInputStream;
    using google::protobuf::io::ZeroCopyInputStream;
    int file_descriptor = open(file_name.c_str(), O_RDONLY);
    if (file_descriptor < 0)
    {
        std::cerr << "Failed to open file " << file_name << " in text mode.";
        // Failed to open;
        return false;
    }

    ZeroCopyInputStream *input = new FileInputStream(file_descriptor);
    bool success = TextFormat::Parse(input, message);
    if (!success)
    {
        std::cerr << "Failed to parse file " << file_name << " as text proto.";
    }
    delete input;
    close(file_descriptor);
    return success;
}

bool SetProtoToBinaryFile(const google::protobuf::Message &message,
                          const std::string &file_name)
{
    std::fstream output(file_name,
                        std::ios::out | std::ios::trunc | std::ios::binary);
    return message.SerializeToOstream(&output);
}

bool GetProtoFromBinaryFile(const std::string &file_name,
                            google::protobuf::Message *message)
{
    std::fstream input(file_name, std::ios::in | std::ios::binary);
    if (!input.good())
    {
        std::cerr << "Failed to open file " << file_name << " in binary mode.";
        return false;
    }
    if (!message->ParseFromIstream(&input))
    {
        std::cerr << "Failed to parse file " << file_name << " as binary proto.";
        return false;
    }
    return true;
}

bool GetProtoFromFile(const std::string &file_name,
                      google::protobuf::Message *message)
{
    // Try the binary parser first if it's much likely a binary proto.
    static const std::string kBinExt = ".bin";
    if (std::equal(kBinExt.rbegin(), kBinExt.rend(), file_name.rbegin()))
    {
        return GetProtoFromBinaryFile(file_name, message) ||
               GetProtoFromASCIIFile(file_name, message);
    }

    return GetProtoFromASCIIFile(file_name, message) ||
           GetProtoFromBinaryFile(file_name, message);
}

bool GetContent(const std::string &file_name, std::string *content)
{
    std::ifstream fin(file_name);
    if (!fin)
    {
        return false;
    }

    std::stringstream str_stream;
    str_stream << fin.rdbuf();
    *content = str_stream.str();
    return true;
}

std::string GetAbsolutePath(const std::string &prefix,
                            const std::string &relative_path)
{
    if (relative_path.empty())
    {
        return prefix;
    }
    // If prefix is empty or relative_path is already absolute.
    if (prefix.empty() || relative_path.front() == '/')
    {
        return relative_path;
    }

    if (prefix.back() == '/')
    {
        return prefix + relative_path;
    }
    return prefix + "/" + relative_path;
}

bool PathExists(const std::string &path)
{
    struct stat info;
    return stat(path.c_str(), &info) == 0;
}

bool DirectoryExists(const std::string &directory_path)
{
    struct stat info;
    return stat(directory_path.c_str(), &info) == 0 && (info.st_mode & S_IFDIR);
}

std::vector<std::string> Glob(const std::string &pattern)
{
    glob_t globs = {};
    std::vector<std::string> results;
    if (glob(pattern.c_str(), GLOB_TILDE, nullptr, &globs) == 0)
    {
        for (size_t i = 0; i < globs.gl_pathc; ++i)
        {
            results.emplace_back(globs.gl_pathv[i]);
        }
    }
    globfree(&globs);
    return results;
}

bool CopyFile(const std::string &from, const std::string &to)
{
    std::ifstream src(from, std::ios::binary);
    if (!src)
    {
        AWARN << "Source path could not be normally opened: " << from;
        std::string command = "cp -r " + from + " " + to;
        ADEBUG << command;
        const int ret = std::system(command.c_str());
        if (ret == 0)
        {
            ADEBUG << "Copy success, command returns " << ret;
            return true;
        }
        else
        {
            ADEBUG << "Copy error, command returns " << ret;
            return false;
        }
    }

    std::ofstream dst(to, std::ios::binary);
    if (!dst)
    {
        std::cerr << "Target path is not writable: " << to;
        return false;
    }

    dst << src.rdbuf();
    return true;
}

bool CopyDir(const std::string &from, const std::string &to)
{
    DIR *directory = opendir(from.c_str());
    if (directory == nullptr)
    {
        std::cerr << "Cannot open directory " << from;
        return false;
    }

    bool ret = true;
    if (EnsureDirectory(to))
    {
        struct dirent *entry;
        while ((entry = readdir(directory)) != nullptr)
        {
            // skip directory_path/. and directory_path/..
            if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
            {
                continue;
            }
            const std::string sub_path_from = from + "/" + entry->d_name;
            const std::string sub_path_to = to + "/" + entry->d_name;
            if (entry->d_type == DT_DIR)
            {
                ret &= CopyDir(sub_path_from, sub_path_to);
            }
            else
            {
                ret &= CopyFile(sub_path_from, sub_path_to);
            }
        }
    }
    else
    {
        std::cerr << "Cannot create target directory " << to;
        ret = false;
    }
    closedir(directory);
    return ret;
}

bool Copy(const std::string &from, const std::string &to)
{
    return DirectoryExists(from) ? CopyDir(from, to) : CopyFile(from, to);
}

bool EnsureDirectory(const std::string &directory_path)
{
    std::string path = directory_path;
    for (size_t i = 1; i < directory_path.size(); ++i)
    {
        if (directory_path[i] == '/')
        {
            // Whenever a '/' is encountered, create a temporary view from
            // the start of the path to the character right before this.
            path[i] = 0;

            if (mkdir(path.c_str(), S_IRWXU) != 0)
            {
                if (errno != EEXIST)
                {
                    return false;
                }
            }

            // Revert the temporary view back to the original.
            path[i] = '/';
        }
    }

    // Make the final (full) directory.
    if (mkdir(path.c_str(), S_IRWXU) != 0)
    {
        if (errno != EEXIST)
        {
            return false;
        }
    }

    return true;
}

bool RemoveAllFiles(const std::string &directory_path)
{
    DIR *directory = opendir(directory_path.c_str());
    if (directory == nullptr)
    {
        std::cerr << "Cannot open directory " << directory_path;
        return false;
    }

    struct dirent *file;
    while ((file = readdir(directory)) != nullptr)
    {
        // skip directory_path/. and directory_path/..
        if (!strcmp(file->d_name, ".") || !strcmp(file->d_name, ".."))
        {
            continue;
        }
        // build the path for each file in the folder
        std::string file_path = directory_path + "/" + file->d_name;
        if (unlink(file_path.c_str()) < 0)
        {
            std::cerr << "Fail to remove file " << file_path << ": " << strerror(errno);
            closedir(directory);
            return false;
        }
    }
    closedir(directory);
    return true;
}

std::vector<std::string> ListSubPaths(const std::string &directory_path,
                                      const unsigned char d_type)
{
    std::vector<std::string> result;
    DIR *directory = opendir(directory_path.c_str());
    if (directory == nullptr)
    {
        std::cerr << "Cannot open directory " << directory_path;
        return result;
    }

    struct dirent *entry;
    while ((entry = readdir(directory)) != nullptr)
    {
        // Skip "." and "..".
        if (entry->d_type == d_type && strcmp(entry->d_name, ".") != 0 &&
            strcmp(entry->d_name, "..") != 0)
        {
            result.emplace_back(entry->d_name);
        }
    }
    closedir(directory);
    return result;
}

std::string GetFileName(const std::string &path, const bool remove_extension)
{
    std::string::size_type start = path.rfind('/');
    if (start == std::string::npos)
    {
        start = 0;
    }
    else
    {
        // Move to the next char after '/'.
        ++start;
    }

    std::string::size_type end = std::string::npos;
    if (remove_extension)
    {
        end = path.rfind('.');
        // The last '.' is found before last '/', ignore.
        if (end != std::string::npos && end < start)
        {
            end = std::string::npos;
        }
    }
    const auto len = (end != std::string::npos) ? end - start : end;
    return path.substr(start, len);
}

std::string GetCurrentPath()
{
    char tmp[PATH_MAX];
    return getcwd(tmp, sizeof(tmp)) ? std::string(tmp) : std::string("");
}

bool GetType(const string &filename, FileType *type)
{
    struct stat stat_buf;
    if (lstat(filename.c_str(), &stat_buf) != 0)
    {
        return false;
    }
    if (S_ISDIR(stat_buf.st_mode) != 0)
    {
        *type = TYPE_DIR;
    }
    else if (S_ISREG(stat_buf.st_mode) != 0)
    {
        *type = TYPE_FILE;
    }
    else
    {
        AWARN << "failed to get type: " << filename;
        return false;
    }
    return true;
}

bool DeleteFile(const string &filename)
{
    if (!PathExists(filename))
    {
        return true;
    }
    FileType type;
    if (!GetType(filename, &type))
    {
        return false;
    }
    if (type == TYPE_FILE)
    {
        if (remove(filename.c_str()) != 0)
        {
            std::cerr << "failed to remove file: " << filename;
            return false;
        }
        return true;
    }
    DIR *dir = opendir(filename.c_str());
    if (dir == nullptr)
    {
        AWARN << "failed to opendir: " << filename;
        return false;
    }
    dirent *dir_info = nullptr;
    while ((dir_info = readdir(dir)) != nullptr)
    {
        if (strcmp(dir_info->d_name, ".") == 0 ||
            strcmp(dir_info->d_name, "..") == 0)
        {
            continue;
        }
        string temp_file = filename + "/" + string(dir_info->d_name);
        FileType temp_type;
        if (!GetType(temp_file, &temp_type))
        {
            AWARN << "failed to get file type: " << temp_file;
            closedir(dir);
            return false;
        }
        if (type == TYPE_DIR)
        {
            DeleteFile(temp_file);
        }
        remove(temp_file.c_str());
    }
    closedir(dir);
    remove(filename.c_str());
    return true;
}

bool CreateDir(const string &dir)
{
    int ret = mkdir(dir.c_str(), S_IRWXU | S_IRWXG | S_IRWXO);
    if (ret != 0)
    {
        AWARN << "failed to create dir. [dir: " << dir
              << "] [err: " << strerror(errno) << "]";
        return false;
    }
    return true;
}
} // namespace common
